Pro ASP.NET Core MVC2(第7版)翻译

第15章:URL 路由

作者:Adam Freeman 翻译:陈广 日期:2018-9-19


早期版本的 ASP.NE T假定所请求的 URL 与服务器硬盘上的文件之间存在直接关系。服务器的工作是接收来自浏览器的请求并从相应的文件中传递输出。这种方法对于 Web Forms 来说是很好的,其中每个 ASPX 页面既是一个文件,也是对请求的一个自包含响应。

对于 MVC 应用程序来说,这是没有意义的,因为在 MVC 应用程序中,请求是由控制器类中的 action 方法处理的,并且与磁盘上的文件没有一对一的关联。

为了处理 MVC URL,ASP.NET 平台使用了路由系统,该系统已经为 ASP.NET Core 进行了大修。在本章中,我将向您展示如何使用路由系统为您的项目创建强大而灵活的 URL 处理。正如您将看到的,路由系统允许您创建任何您想要的 URL 模式,并以干净、简洁的方式表示它们。路由系统有两个功能。

  • 检查传入 URL 并选择控制器和 action 来处理请求。
  • 生成传出 URL。这些 URL 出现在视图呈现的 HTML 中,以便在用户单击链接时调用特定的 action(此时它再次成为传入 URL)。

本章我将着重于定义路由并使用它们来处理传入 URL,以便用户能够到达控制器和 action。在 MVC 应用程序中创建路由有两种方法:基于约定的路由属性路由。我在本章中解释了这两种方法。

然后,在下一章中,我将向您展示如何使用相同的路由来生成您需要包含在视图中的传出 URL,并向您展示如何自定义路由系统并使用名为 areas 的相关功能。表15-1为路由简介。

表 15-1:路由简介

问题 回答
它是什么? 路由系统负责处理传入的请求,并选择处理它们的控制器和 action 方法。路由系统还用于生成视图中的路由,称为传出 URL。
为什么有用? 路由系统允许灵活地处理请求,而不需要将 URL 绑定到 Visual Studio项目中的类结构。
它是如何使用的? URL 与控制器和 action 方法之间的映射在 Startup.cs 文件中定义,或者通过将路由属性应用于控制器来定义。
是否有任何缺陷或限制? 复杂应用程序的路由配置很难管理。
有没有其他选择? 没有,路由系统是 ASP.NET Core 不可分割的一部分。

表 15-2:本章摘要

问题 解决方案 清单
URL 和 action 方法之间的映射 定义一个路由 9
允许省略 URL 段 定义路由段的默认值 10-12
匹配没有相应路由变量的 URL 段 定义静态段 13-16
将 URL 段传递给 action 方法。 定义自定义段变量 17-19
允许省略没有默认值的 URL 段 定义可选段 20-21
定义与任意数量的 URL 段匹配的路由 使 Catchall 段 22-23
限制路由可以匹配的 URL 应用路由约束 24-33
在控制器内定义路由 使用属性路由 34-38

准备示例项目

本章,我使用【ASP.NET Core Web 应用程序(.NET Core)】模板创建了一个名为 UrlsAndRoutes 的【空】项目。为添加 MVC 框架、开发者错误页和静态文件的支持,我在Startup类中添加了清单15-1所示的代码。

清单 15-1:UrlsAndRoutes 文件夹下的 Startup.cs 文件,配置应用程序

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc();
        }
    }
}

创建模型类

本章的所有工作都是将请求 URL 与 action 匹配。我需要的唯一模型类是传递有关已选择用于处理请求的控制器和 action 方法的详细信息。我创建了 Models 文件夹,并添加了一个名为 Result.cs 的类文件,用于定义清单15-2所示的类。

清单 15-2:Models 文件夹下的 Result.cs 文件的内容

using System.Collections.Generic;

namespace UrlsAndRoutes.Models
{
    public class Result
    {
        public string Controller { get; set; }
        public string Action { get; set; }
        public IDictionary<string, object> Data { get; }
            = new Dictionary<string, object>();
    }
}

ControllerAction属性将用于指示如何处理请求,数据字典将用于存储有关路由系统产生的请求的其他详细信息。

创建示例控制器

我需要一些简单的控制器来演示路由工作。我创建了 Controllers 文件夹,并添加了一个名为 HomeController.cs的类文件,其内容如清单15-3所示。

清单 15-3:Controllers 文件夹下的 HomeController.cs 文件的内容

using Microsoft.AspNetCore.Mvc;
using UrlsAndRoutes.Models;

namespace UrlsAndRoutes.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index() => View("Result",
            new Result
            {
                Controller = nameof(HomeController),
                Action = nameof(Index)
            });
    }
}

Home 控制器所定义的Index action 方法调用View方法来渲染名为Result的视图(我会在下一节定义),并提供了一个Result对象作为模型对象。模型对象的属性使用nameof函数设置,它们指示用于服务请求的控制器和 aciton 方法。

我遵循同样的模式,向 Controllers 文件夹中添加了一个 CustomerController.cs 文件,并使用它定义了如清单15-4所示的 Customer 控制器。

清单 15-4:Controllers 文件夹下的 CustomerController.cs 文件的内容

using Microsoft.AspNetCore.Mvc;
using UrlsAndRoutes.Models;

namespace UrlsAndRoutes.Controllers
{
    public class CustomerController : Controller
    {
        public ViewResult Index() => View("Result",
            new Result
            {
                Controller = nameof(CustomerController),
                Action = nameof(Index)
            });

        public ViewResult List() => View("Result",
            new Result
            {
                Controller = nameof(CustomerController),
                Action = nameof(List)
            });
    }
}

第三个也是最后一个控制器是在一个名为 AdminController.cs 的文件中定义的,我将其添加到 Controllers 文件夹中,如清单15-5所示,它遵循与其他控制器相同的模式。

清单 15-5:Controllers 文件夹下的 AdminController.cs 文件的内容

using Microsoft.AspNetCore.Mvc;
using UrlsAndRoutes.Models;

namespace UrlsAndRoutes.Controllers
{
    public class AdminController : Controller
    {
        public ViewResult Index() => View("Result",
            new Result
            {
                Controller = nameof(AdminController),
                Action = nameof(Index)
            });
    }
}

创建视图

在上一节中定义的所有 action 方法都指向了 Result 视图,这使得我可以创建一个将由所有控制器共享的视图。我创建了 Views/Shared 文件夹并添加了一个名为 Result.cshtml 的新视图,内容如清单15-6所示。

清单 15-6:Views/Shared 文件夹下的 Result.cshtml 文件的内容

@model Result
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Routing</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/*.min.css" />
</head>
<body class="m-1 p-1">
    <table class="table table-bordered table-striped table-sm">
        <tr><th>Controller:</th><td>@Model.Controller</td></tr>
        <tr><th>Action:</th><td>@Model.Action</td></tr>
        @foreach (string key in Model.Data.Keys)
        {
            <tr><th>@key :</th><td>@Model.Data[key]</td></tr>
        }
    </table>
</body>
</html>

该视图包含一个表,它在表中显示来自模型对象的属性,并使用了 Bootstrap 样式。为了将 Bootstrap 添加到项目中,我在 UrlsAndRoutes 项目中单击鼠标右键,在弹出菜单中选择【添加】➤【添加客户端库】,并将 twitter-bootstrap 添加至项目中。最终生成的 libman.json 配置文件代码如下:

清单 15-7:UrlsAndRoutes 文件夹下的 libman.json 文件,添加 Bootstrap 包

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "twitter-bootstrap@4.1.3",
      "destination": "wwwroot/lib/twitter-bootstrap/"
    }
  ]
}

最后的准备工作是在 Views 文件夹中创建 _ViewImports.cshtml 文件,该文件设置内置标签助手,以便在 Razor 视图中使用,并导入模型命名空间,如清单15-8所示。

清单 15-8:Views 文件夹下的 _ViewImports.cshtml 文件的内容

@using UrlsAndRoutes.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Startup类中的配置不包含任何关于 MVC 应该如何将 HTTP 请求映射到控制器和 action 的指令。当您启动应用程序时,所请求的任何 URL 都会产生一个【404 - Not Found】的响应,如图15-1所示。

图15-1 运行示例应用程序

引入 URL 模式

路由系统使用一组路由来施展魔力。这些路由集合包括应用程序架构和 URL 架构,这是应用程序将识别和响应的一组 URL。

我不需要手动键入应用程序支持的所有的单个 URL。相反,每个路由包含一个 URL 模式,用于对比传入 URL。如果传入 URL 与模式匹配,则路由系统将使用它来处理该 URL。我们从一个简单的 URL开始:

http://mysite.com/Admin/Index

URL可以细分为段(segment)。它是 URL 的一部分,不包括主机名和查询字符串,由/字符分隔。在示例 URL 中,有两个段,如图15-2所示。

图15-2 示例 URL 中的段

第一个段所包含的单词为Admin,第二个段包含单词Index。在人眼看来,很明显,第一段与控制器有关,第二段与 action 有关。当然,我需要使用路由系统可以理解的 URL 模式来表示这种关系。下面是一个与示例 URL 匹配的 URL 模式:

{controller}/{action}

当处理传入 HTTP 请求时,路由系统的任务是匹配已请求到一个模式的 URL,并从 URL 中提取模式中定义的段变量的值。

段变量用大括号{}字符表示,示例模式有两个名为controlleraction的段变量,因此controller段变量的值将是Adminaction段变量的值将是Index

MVC 应用程序通常有几个路由,路由系统将传入 URL 与每个路由的 URL 模式进行比较,直到找到匹配的路由。默认情况下,模式将匹配具有正确分段数的任何 URL。例如,模式{controller}/{action}将匹配有两个段的任何URL,如表15-3所述。

表 15-3:匹配 URL

请求 URL 段变量
http://mysite.com/Admin/Index controller = Admin action = Index
http://mysite.com/Admin 不匹配 —— 段太少
http://mysite.com/Admin/Index/Soccer 不匹配 —— 段太多

表15-3突出了 URL 模式的两个关键行为。

  • URL 模式对于它们匹配的段数是保守的。它们将只匹配与模式拥有相同段数的 URL。您可以在表中的第二个和第三个例子中看到这一点。
  • URL 模式对于它们匹配的段的内容是自由的。如果 URL 有正确的段数,模式将提取一个段变量中每个段的值,不管它是什么。

这些是默认行为,这是理解 URL 模式如何工作的关键。我将在本章后面向您展示如何更改默认值。

创建并注册一个简单路由

一旦考虑到 URL 模式,就可以使用它来定义路由。路由在 Startup.cs 文件中定义,并作为参数传递给UseMvc方法,该方法用于在Configure方法中设置 MVC。清单15-9显示了将请求映射到示例应用程序中的控制器的基本路由。

清单 15-9:UrlsAndRoutes 文件夹下的 Startup.cs 文件,定义一个基本路由

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(name: "default", template: "{controller}/{action}");
            });
        }
    }
}

路由是使用作为参数传递给UseMvc配置方法的 lambda 表达式创建的。表达式接收实现了Microsoft.AspNetCore.Routing命名空间中的IRouteBuilder接口的对象,并使用MapRoute扩展方法定义路由。为了使路由更容易理解,约定是在调用MapRoute方法时指定参数名,这就是为什么我在清单中显式地命名了nametemplate参数。name参数指定路由的名称,template参数用于定义模式。

提示:命名路由是可选的,有一个哲学的观点,这样做会牺牲一些干净的关注点分离,否则这些分离会来自路由。我在第16章的《从指定路由生成一个 URL》这一节解释了它为什么会成为一个问题。

通过启动示例应用程序,您可以看到我对路由所做的更改的效果。应用程序第一次启动时没有发生任何更改 —— 您仍然会看到 404 错误 —— 但是如果导航到与{controller}/{action}模式匹配的 URL,您将看到如图15-3所示的结果,它演示了导航到 /Admin/Index 的效果。

图15-3 使用简单路由导航

应用程序的根 URL 不能工作的原因是,我添加到 Startup.cs 文件中的路由没有告诉 MVC,当所请求的 URL 没有分段的情况下如何选择控制器类和 action 方法。我将在下一节中修复这个问题。

定义默认值

当请求默认 URL 时,示例应用程序返回 404 消息,因为它与在Startup类中定义的路由模式不匹配。由于默认 URL 中没有可以与路由模式定义的controlleraction变量相匹配的段,所以路由系统不匹配。

我在前面解释过,URL 模式将只匹配指定数量的段的 URL。更改此行为的一种方法是使用默认值。当 URL 不包含路由模式匹配的段时,将使用默认值。清单15-10定义了使用默认值的路由。

清单 15-10:UrlsAndRoutes 文件夹下的 Startup.cs 文件,提供默认值

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes => {
                routes.MapRoute(
                    name: "default",
                    template: "{controller}/{action}",
                    defaults: new { action = "Index" });
            });
        }
    }
}

默认值以匿名类型的属性形式提供,作为defaults参数传递给MapRoute方法。在清单中,我为action变量提供了一个Index默认值。

和以前一样,此路由将匹配所有的两段 URL。例如,如果请求 URL 为 http://mydomain.com/Home/Index,该路由将提取 HOME 作为controller的值,并提取Index作为action的值。

但是,现在action段有一个默认值,路由也将匹配单段 URL。当处理单段 URL 时,路由系统将从 URL 中提取controller值,并为action变量使用默认值。这样,用户就可以请求 /Home,MVC 将在 Home 控制器上调用Index action 方法,如图15-4所示。

图15-4 使用默认 action

定义内联默认值

默认值也可以表示为 URL 模式的一部分,这是表示路由的一种更简洁的方法,如清单15-11所示。内联语法只能为作为 URL 模式一部分的变量提供默认值,但是,正如您将要了解的那样,在该模式之外提供默认值通常是有用的。因此,两种表示默认值的方法都要理解。

清单 15-11:UrlsAndRoutes 文件夹下的 Startup.cs 文件,定义默认值

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller}/{action=Index}");
            });
        }
    }
}

我可以更进一步,匹配根本不包含任何段变量的 URL,只依赖默认值来标识 action 和控制器。作为示例,清单15-12展示了如何通过为两个段提供默认值来映射应用程序的根 URL。

清单 15-12:UrlsAndRoutes 文件夹下的 Startup.cs 文件,提供默认值

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}");
            });
        }
    }
}

通过为controlleraction变量提供默认值,路由将匹配具有零个、一个或两个段的URL,如表15-4所示。

表 15-4:匹配 URL

示例 映射为
0 / controller = Home action = Index
1 /Customer controller = Customer action = Index
2 /Customer/List controller = Customer action = List
3 /Customer/List/All 不匹配 —— 太多段

传入 URL 中收到的段越少,路由就越依赖于默认值,直到只使用默认值匹配没有段的 URL 为止。

通过启动示例应用程序,您可以看到默认值的效果。当浏览器请求应用程序的根 URL 时,将使用controlleraction段变量的默认值,这导致 MVC 调用 Home 控制器上的Index action 方法,如图15-5所示。

图15-5 使用默认值扩展路由的范围

使用静态 URL 段

并不是 URL 模式中的所有段都需要变量。您还可以创建具有*静态段(static segments)*的模式。假设应用程序需要匹配以Public为前缀的 URL,如下所示:

http://mydomain.com/Public/Home/Index

可以通过实现清单15-13所示的 URL 模式来实现它。

清单 15-13:UrlsAndRoutes 文件夹下的 Startup.cs 文件,使用静态段

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}");

                routes.MapRoute(
                    name: "",
                    template: "Public/{controller=Home}/{action=Index}");
            });
        }
    }
}

这个新模式将只匹配包含三个段的 URL,其中第一个段必须为Public。另外两个段可以包含任何值,并将用于controlleraction变量。如果省略最后两个段,则将使用默认值。

您还可以创建包含静态和可变元素段的 URL 模式,如清单15-14所示。

清单 15-14:UrlsAndRoutes 文件夹下的 Startup.cs 文件,混合段

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute("", "X{controller}/{action}");

                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}");

                routes.MapRoute(
                    name: "",
                    template: "Public/{controller=Home}/{action=Index}");
            });
        }
    }
}

此路由中的模式匹配任何两段 URL,其中第一段以字母X开头。controller的值来自第一段,不包括Xaction值取自第二个段。如果启动应用程序并导航到 /XHome/Index,则可以看到此路由的效果,其结果如图15-6所示。

图15-6 在单个段中混合静态和可变元素


路由排序

在清单15-14中,我定义了一个新路由,并将其置于所有其他路由之前。我这样做是因为路由是按照定义它们的顺序应用的。MapRoute方法将路由添加到路由配置的末尾,这意味着路由一般按照定义它们的顺序应用。我说“一般”是因为有些方法会将路由插入到特定的位置。我倾向于不使用这些方法,因为按照定义的顺序应用路由可以简化对应用程序路由的理解。

路由系统尝试将传入 URL 与第一个定义的路由 URL 模式相匹配,只有在没有匹配的情况下才会继续到下一个路由。这些路由是按顺序尝试的,直到找到匹配或一组路由已经用尽为止。因此,必须首先定义最具体的路由。我在清单15-14中添加的路由比下面的路由更具体。假设颠倒了路由的顺序,如下所示:

...
routes.MapRoute("MyRoute", "{controller=Home}/{action=Index}");
routes.MapRoute("", "X{controller}/{action}");
...

那么,由于第一个路由将匹配任何具有0、1或2段的 URL,它将始终会被使用。更具体的路由,现在是列表中的第二个,将永远用不上。新路由不包括 URL 的前导X,而且不会由旧路由完成。因此,这样的 URL:
http://mydomain.com/XHome/Index
将目标指向名为XHome的控制器,并假设应用程序中有XHomeController类,并且它有一个名为Index的 action 方法。


静态 URL 段和默认值可以结合起来为特定的 URL 创建别名。在部署应用程序时,使用的 URL 架构与用户形成契约,如果随后重构应用程序,则需要保留先前的 URL 格式,以便用户创建的任何 URL 收藏夹、宏或脚本都能继续工作。

假设曾经有一个名为 Shop 的控制器,现在它已经被 Home 控制器取代了。清单15-15展示了我如何创建一条路径来保存旧的 URL 模式。

清单 15-15:UrlsAndRoutes 文件夹下的 Startup.cs 文件,段和默认值

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "ShopSchema",
                    template: "Shop/{action}",
                    defaults: new { controller = "Home" });

                routes.MapRoute("", "X{controller}/{action}");

                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}");

                routes.MapRoute(
                    name: "",
                    template: "Public/{controller=Home}/{action=Index}");
            });
        }
    }
}

该路由匹配任何两段 URL,其中第一段是Shopaction值取自第二个 URL 段。URL 模式不包含controller的变量段,因此使用默认值。defaults参数提供controller值,因为没有任何段可以将值作为 URL 模式的一部分应用到其中。

结果是,将对 Shop 控制器的操作请求转换为对 Home 控制器的请求。通过启动应用程序并导航到 /Shop/Index URL,您可以看到此路由的效果。如图15-7所示,新路由导致 MV C在 Home 控制器中以Index action 方法为目标。

图15-7 创建别名以保留 URL 架构

我可以更进一步,为已经重构并不再存在于控制器中的 action 方法创建别名。为此,我创建一个静态 URL,并将controlleraction值作为默认值提供,如清单15-16所示。

清单 15-16:UrlsAndRoutes 文件夹下的 Startup.cs 文件,控制器和 action 别名

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "ShopSchema2",
                    template: "Shop/OldAction",
                    defaults: new { controller = "Home", action = "Index" });

                routes.MapRoute(
                    name: "ShopSchema",
                    template: "Shop/{action}",
                    defaults: new { controller = "Home" });

                routes.MapRoute("", "X{controller}/{action}");

                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}");

                routes.MapRoute(
                    name: "",
                    template: "Public/{controller=Home}/{action=Index}");
            });
        }
    }
}

请注意,新路由首先定义,因为它比之后的路由更具体。例如,如果对 Shop/OldAction 的请求由下一个定义的路由处理,那么如果存在具有OldAction action 方法的控制器,则会得到与我想要的不同的结果。

定义自定义段变量

controlleraction段变量在 MVC 应用程序中具有特殊的意义,它们对应于将用于服务请求的控制器和 action 方法。这些只是内置的段变量,还可以定义自定义段变量,如清单15至17所示。(我已经删除了前面小节的路由,以便从头开始)。

清单 15-17:UrlsAndRoutes 文件夹下的 Startup.cs 文件,定义附加变量

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(name: "MyRoute",
                    template: "{controller=Home}/{action=Index}/{id=DefaultId}");
            });
        }
    }
}

URL 模式定义了标准controlleraction变量,以及一个名为id的自定义变量。这个路由将匹配任何0到3段的 URL。第三个段的内容将分配给id变量,如果没有第三个段,则使用默认值。

警告:有些名称是保留的,不可用于自定义段变量名。它们是 controller、action、area 和 page。前两个的意思是显而易见的。我在下一章中解释 areas,而 page 被 Razor 页面功能所使用。

Controller类是控制器的基础,它定义了一个RouteData属性,该属性返回Microsoft.AspNetCore.Routing.RouteData对象,并提供有关路由系统和当前请求的路由方式的详细信息。在控制器中,我可以使用RouteData.Values属性访问 action 方法中的任何段变量,该属性返回包含段变量的字典。为了演示,我向 Home 控制器添加了一个 action 方法,名为CustomVariable,如清单15-18所示。

清单 15-18:Controllers 文件夹下的 HomeController.cs 文件,访问段变量

using Microsoft.AspNetCore.Mvc;
using UrlsAndRoutes.Models;

namespace UrlsAndRoutes.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index() => View("Result",
            new Result
            {
                Controller = nameof(HomeController),
                Action = nameof(Index)
            });

        public ViewResult CustomVariable()
        {
            Result r = new Result
            {
                Controller = nameof(HomeController),
                Action = nameof(CustomVariable),
            };
            r.Data["Id"] = RouteData.Values["id"];
            return View("Result", r);
        }
    }
}

此 action 方法使用RouteData.Values属性在路由 URL 模式中获取自定义id变量的值,该属性返回路由系统产生的变量的字典。自定义变量被添加到视图模型对象中,可以通过运行应用程序和请求以下 URL 来查看:

/Home/CustomVariable/Hello

路由模板与此 URL 中的第三个段匹配,并成为id变量的值,结果如图15-8所示。

图15-8 显示自定义段变量的值

清单15-17中的 URL 模式定义了id段的默认值,这意味着路由也可以匹配有两个段的URL。通过请求以下 URL,您可以看到默认值的使用情况:

/Home/CustomVariable

路由系统使用自定义变量的默认值,如图15-9所示。

图15-9 自定义段变量的默认值

使用自定义变量作为 action 方法参数

使用RouteData.Values集合只是访问自定义路由变量的一种方式,而另一种方式可能更优雅。如果 action 方法定义名称与 URL 模式变量匹配的参数,则 MVC 将自动将从 URL 获得的值作为参数传递给 action 方法。

清单15-17中定义的自定义变量称为id。我可以修改 HOme 控制器中的CustomVariable action 方法,使其具有同名的参数,如清单15-19所示。

清单 15-19:Controllers 文件夹下的 HomeController.cs 文件,添加 Action 参数

using Microsoft.AspNetCore.Mvc;
using UrlsAndRoutes.Models;

namespace UrlsAndRoutes.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index() => View("Result",
            new Result
            {
                Controller = nameof(HomeController),
                Action = nameof(Index)
            });

        public ViewResult CustomVariable(string id)
        {
            Result r = new Result
            {
                Controller = nameof(HomeController),
                Action = nameof(CustomVariable),
            };
            r.Data["Id"] = id;
            return View("Result", r);
        }
    }
}

当路由系统与清单15-17中定义的路由的 URL 匹配时,URL 中的第三个段的值将分配给自定义变量id。MVC 将段变量列表与 action 方法参数列表进行比较,如果名称匹配,则将值从 URL 传递给方法。

id参数的类型是字符串,但 MVC 会尝试将 URL 值转换为使用的任何参数类型。如果 action 方法将id参数声明为intDateTime,那么它将把从 URL 接收值转换为该类型的实例。这是一个优雅而有用的特性,它无需我自己进行转换。通过启动应用程序并请求 /Home/CustomVariable/Hello,您可以看到 action 方法参数的效果,如图15-10所示。如果省略第三个段,则 action 方法将提供默认段值,如图所示。

注意:MVC 使用模型绑定特性将 URL 中包含的值转换为 .NET 类型,并且模型绑定可以处理比本例中显示的更复杂的情况。我在第26章中描述了模型绑定。

图15-10 使用 action 方法参数访问段变量

定义可选 URL 段

可选的 URL 段是用户不需要指定且未指定默认值的部分。可选段在段名后用问号?表示,如清单15-20所示。

清单 15-20:UrlsAndRoutes 文件夹下的 Startup.cs 文件,指定可选段

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(name: "MyRoute",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

无论id段是否已被提供,此路由都将匹配 URL。表15-5显示了它对于不同的 URL 是如何工作的。

表 15-5:可选段变量的 URL 匹配

示例 URL 映射
0 / controller = Home action = Index
1 /Customer controller = Customer action = Index
2 /Customer/List controller = Customer action = List
3 /Customer/List/All controller = Customer action = List id = All
4 /Customer/List/All/Delete 不匹配,太多段

从表中可以看到,只有当传入 URL 中有相应的段时,id变量才会添加到变量集中。如果需要知道用户是否为段变量提供了值,则此功能非常有用。如果没有为可选段变量提供任何值,则相应参数的值将为null。在清单15-21中我更新了 Home 控制器,以在没有为id段变量提供值时进行响应。

清单 15-21:Controllers 文件夹下的 HomeController.cs 文件,检查段

using Microsoft.AspNetCore.Mvc;
using UrlsAndRoutes.Models;

namespace UrlsAndRoutes.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index() => View("Result",
            new Result
            {
                Controller = nameof(HomeController),
                Action = nameof(Index)
            });

        public ViewResult CustomVariable(string id)
        {
            Result r = new Result
            {
                Controller = nameof(HomeController),
                Action = nameof(CustomVariable),
            };
            r.Data["Id"] = id ?? "<no value>";
            return View("Result", r);
        }
    }
}

图15-11显示了启动应用程序并导航到 /Home/CustomVariable URL 的结果,此 URL 不包括id段变量的值。

图15-11 当 URL 不包含可选段变量的值时的检测


理解默认路由配置

当您将 MVC 添加到Startup类中时,可以使用UseMvcWithDefaultRoute方法。这只是设置最常见的路由配置的一种方便方法,相当于以下代码:

...
app.UseMvc(routes => {
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});
...

此默认配置通过名称匹配针对控制器类和 action 方法的 URL,并携带一个可选的id段。如果controlleraction段丢失,则使用默认值分别针对 Home 控制器和Index action 方法。


定义可变长路由

更改 URL 模式的默认保守性的另一种方法是接受可变数量的 URL 段。这允许您在单个路由中导航任意长度的 URL。您可以通过将其中一个段变量指定为 catchall 来定义对变量段的支持,这是通过以星号*作为前缀来完成的,如清单15-22所示。

译者注:catchall 从字面意思上说是捕获所有,表示包罗万向的,本想翻译为“万能”、“通配”,但发现有几个地方这么译并不合适,想来想去干脆不翻,直接用英文得了。

清单 15-22:UrlsAndRoutes 文件夹下的 Startup.cs 文件,指定 Catche 变量

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(name: "MyRoute",
                    template: "{controller=Home}/{action=Index}/{id?}/{*catchall}");
            });
        }
    }
}

我在上面的例子中扩展了路由,添加了一个 catchall 段变量,并想当然地称之为catchall。该路由现在将匹配任何 URL,而不管它包含多少段或其中任何段的值。前三个段分别用于设置controlleractionid变量的值。如果 URL 包含额外的段,则它们都分配给catchall变量,如表15-6所示。

表 15-6:带 Catchall 段变量 URL 的匹配

示例 URL 映射到
0 / controller = Home action = Index
1 /Customer controller = Customer action = Index
2 /Customer/List controller = Customer action = List
3 /Customer/List/All controller = Customer action = List id = All
4 /Customer/List/All/Delete controller = Customer action = List id = All catchall = Delete
5 /Customer/List/All/Delete/Perm controller = Customer action = List id = All catchall = Delete/Perm

在清单15-23中,我更新了 Customer 控制器,以便List action 通过模型对象将 catchall 变量的值传递到视图。

清单 15-23:Controllers 文件夹下的 CustomerController.cs 文件,更新 Action

using Microsoft.AspNetCore.Mvc;
using UrlsAndRoutes.Models;

namespace UrlsAndRoutes.Controllers
{
    public class CustomerController : Controller
    {
        public ViewResult Index() => View("Result",
            new Result
            {
                Controller = nameof(CustomerController),
                Action = nameof(Index)
            });

        public ViewResult List(string id)
        {
            Result r = new Result
            {
                Controller = nameof(HomeController),
                Action = nameof(List),
            };
            r.Data["Id"] = id ?? "<no value>";
            r.Data["catchall"] = RouteData.Values["catchall"];
            return View("Result", r);
        }
    }
}

要测试 catchall 段,运行应用程序并请求如下 URL:

/Customer/List/Hello/1/2/3

该路由中的 URL 模式将匹配的段数没有上限。图15-12显示了所有 catchall 段的效果。请注意,由 catchall 捕捉到的段是以 segment/segment/segment 的形式显示的,我负责处理字符串以分离各个段。

图15-12 使用 catchall 段

约束路由

在本章的开头,我描述了当 URL 模式与 URL 中的段数匹配时的保守性,并且当它们与段的内容匹配时又是自由的。前面几节解释了控制保守程度的不同技术:使用默认值、可选变量等可使路由匹配得更多或更少。

现在是时候研究在匹配 URL 段的内容时,如何控制自由度,也就是如何限制与一个路由匹配的 URL 集合。清单15-24演示了如何使用简单约束来限制与路由匹配的 URL。

清单 15-24:UrlsAndRoutes 文件夹下的 Startup.cs 文件,约束路由

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(name: "MyRoute",
                    template: "{controller=Home}/{action=Index}/{id:int?}");
            });
        }
    }
}

约束使用冒号:与段变量名分隔。清单中的约束是int,它已应用于id段。这是内联约束的一个示例,它被定义为应用于单个段的 URL 模式的一部分:

template: "{controller}/{action}/{id:int?}";

int约束只允许 URL 模式匹配可以将其值解析为整数值的段。id段是可选的,因此路由将匹配省略id段的段,但是如果存在该段,那么它必须是整数值,如表15-7所总结的那样。

表 15-7:匹配带约束的 URL

示例 URL 映射为
/ controller = Home action = Index id = null
/Home/CustomVariable/Hello 不匹配 —— id段不能被解析为int
/Home/CustomVariable/1 controller = Home action = CustomVariable id = 1
/Home/CustomVariable/1/2 不匹配 —— 太多段

约束可以在 URL 模式之外指定,在定义路由时,可在MapRoute方法中使用constraints参数。如果您希望将 URL 模式与其约束分开,或者您更喜欢遵循早期版本的 MVC 所使用的路由样式(早期 MVC 版本不支持内联约束),则此技术非常有用。清单15-25显示了id段变量上相同效果的整数约束,它使用单独的约束表示。当使用这种格式时,默认值也是外部表示的。

清单 15-25:UrlsAndRoutes 文件夹下的 Startup.cs 文件,表示约束

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing.Constraints;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "MyRoute",
                    template: "{controller}/{action}/{id?}",
                    defaults: new { controller = "Home", action = "Index" },
                    constraints: new { id = new IntRouteConstraint() });
            });
        }
    }
}

MapRoute方法的constraints参数是使用匿名类型定义的,其属性名对应于受约束的段变量。Microsoft.AspNetCore.Routing.Constraints命名空间包含一组可用于定义单个约束的类。在清单15-25中,constraints参数被配置为id段使用IntRouteConstraint对象,创建与清单15-24所示的内联约束相同的效果。

表15-8描述了Microsoft.AspNetCore.Routing.Constraints命名空间中的完整约束类集合,以及可以应用于 URL 模式中单个段的约束的内联等效项,我将在下面的部分中对其中的一些约束进行描述。

提示:您可以使用 MVC 提供的一组特性(如HttpGetHttpPost)限制对使用特定 HTTP 谓词(如 GET 或 POST )发出的请求的 action 方法的访问。有关使用这些特性在控制器中处理表单的详细信息,请参阅第7章;有关可用特性的完整列表,请参见第20章。

表 15-8:段级路由约束

内联约束 描述 类名
alpha 匹配字母表字符,不论大小写(A-Z,a-z) AlphaRouteConstraint()
bool 匹配可以解析为bool的值 BoolRouteConstraint()
datetime 匹配可以解析为DateTime的值 DateTimeRouteConstraint()
decimal 匹配可解析为decimal的值 DecimalRouteConstraint()
double 匹配可解析为double的值 DoubleRouteConstraint()
float 匹配可解析为float的值 FloatRouteConstraint()
guid 将值与全局唯一标识符匹配 GuidRouteConstraint()
int 匹配可解析为int的值 IntRouteConstraint()
length(len) length(min, max) 匹配指定字符数的值,或长度介于 min 和 max 之间的值(包括) LengthRouteConstraint(len)
LengthRouteConstraint(min, max)
long 匹配可解析为long的值 LongRouteConstraint()
maxlength(len) 匹配不超过 len 个字符的字符串 MaxLengthRouteConstraint(len)
max(val) 匹配小于 val 的int MaxRouteConstraint(val)
minlength(len) 匹配至少有 len 个字符的字符串 MinLengthRouteConstraint(len)
min(val) 匹配大于 val 的int MinRouteConstraint(val)
range(min, max) 匹配介于 min 和 max 之间(包括)的int
regex(expr) 匹配一个正则表达式 RegexRouteConstraint(expr)

使用正则表达式约束路由

提供最大灵活性的约束是regex,它使用正则表达式匹配一个段。在清单15-26中,我限制了控制器段以限制它将匹配的 URL 范围。

清单 15-26:UrlsAndRoutes 文件夹下的 Startup.cs 文件,使用正则表达式

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing.Constraints;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "MyRoute",
                    template: "{controller:regex(^H.*)=Home}/{action=Index}/{id?}");
            });
        }
    }
}

我使用的约束限制了路由,以便它只能匹配controller段以字母H开头的 URL。

注意:默认值是在检查约束之前应用的。因此,例如,如果我请求的 URL 为 /,则应用控制器的默认值 Home。然后检查约束,并且由于controller值以H开头,默认 URL 将与路由匹配。

正则表达式可以约束路由,因此只有 URL 段的特定值才会导致匹配。这是使用 bar |字符完成的,如清单15-27所示。(我将 URL 模式分为两部分,以便它适合页面,这在实际项目中不需要担心)。

清单 15-27:UrlsAndRoutes 文件夹下的 Startup.cs 文件,约束路由

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing.Constraints;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "MyRoute",
                    template: "{controller:regex(^H.*)=Home}/"
                        + "{action:regex(^Index$|^About$)=Index}/{id?}");
            });
        }
    }
}

此约束将允许路由仅匹配那些 action 段的值为IndexAbout的 URL。约束同时应用,因此对action变量的值施加的限制与施加在controller变量上的约束相结合。这意味着,清单15-27中的路由只有在controller变量以字母H开头且action变量为IndexAbout时才与URL匹配。

使用类型和值约束

大多数约束用于限制路由,以让它们只匹配可以转换为指定类型或具有特定格式的段的 URL。我在本节开头使用的int约束是一个很好的例子:只有当约束段的值可以解析为 .NET 的int值时,它才会匹配路由。清单15-28演示了range约束的使用,该约束限制了路由,使其仅在段值可以转换为int并在指定值之间时才匹配 URL。

清单 15-28:UrlsAndRoutes 文件夹下的 Startup.cs 文件,基于类型和值的约束

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing.Constraints;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "MyRoute",
                    template: "{controller=Home}/{action=Index}/{id:range(10,20)?}");
            });
        }
    }
}

此示例中的约束应用于可选的id段。如果请求 URL 没有至少三个段,则将忽略该约束。如果id段存在,则只有当段值可以转换为int且值介于10和20之间时,路由才会匹配 URL。范围约束是包含的,这意味着10和20被认为在范围内。

组合约束

如果需要对单个段应用多个约束,那么将它们链接在一起,约束之间用冒号分隔,如清单15-29所示。

清单 15-29:UrlsAndRoutes 文件夹下的 Startup.cs 文件,组合内联约束

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing.Constraints;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "MyRoute",
                    template: "{controller=Home}/{action=Index}"
                        + "/{id:alpha:minlength(6)?}");
            });
        }
    }
}

在这个清单中,我对id段同时应用了alphaminlength约束。表示可选段的问号是在所有约束之后应用的。组合这些约束的效果是,只有在省略id段(因为它是可选的)或存在并且包含至少6个字母表字符的情况下,路由才会匹配 URL。

如果不使用内联约束,则必须使用Microsoft.AspNetCore.Routing.CompositeRouteConstraint类,它允许多个约束与匿名类型对象中的单个属性相关联。清单15-30演示了我在清单15-29中使用的约束的组合。

清单 15-30:UrlsAndRoutes 文件夹下的 Startup.cs 文件,组合独立约束

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "MyRoute",
                    template: "{controller}/{action}/{id?}",
                    defaults: new { controller = "Home", action = "Index" },
                    constraints: new
                    {
                        id = new CompositeRouteConstraint(
                            new IRouteConstraint[] {
                                new AlphaRouteConstraint(),
                                new MinLengthRouteConstraint(6)
                            })
                    });
            });
        }
    }
}

CompositeRouteConstraint类的构造函数接受实现IRouteConstraint对象的枚举,它是定义路由约束的接口。只有在满足所有约束的情况下,路由系统才允许路由匹配 URL。

定义自定义约束

如果标准约束无法满足您的需求,则可以通过实现IRouteConstraint接口来定义您自己的自定义约束,该接口是在Microsoft.AspNetCore.Routing命名空间中定义的。为了演示这一特性,我在示例项目中添加了一个 Infrastructure 文件夹,并创建了一个新的类文件,名为 WeekDayConstraint.cs,其内容如清单15-31所示。

清单 15-31:Infrastructure 文件夹下的 WeekDayConstraint.cs 文件的内容

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using System.Linq;
namespace UrlsAndRoutes.Infrastructure
{
    public class WeekDayConstraint : IRouteConstraint
    {
        private static string[] Days = new[] { "mon", "tue", "wed", "thu", "fri", "sat", "sun" };

        public bool Match(HttpContext httpContext, IRouter route,
            string routeKey, RouteValueDictionary values,
            RouteDirection routeDirection)
        {
            return Days.Contains(values[routeKey]?.ToString().ToLowerInvariant());
        }
    }
}

IRouteConstraint接口定义Match方法,该方法被调用以允许约束决定请求是否应该被路由匹配。Match方法的参数提供了如下内容:对来自客户端的请求的访问,路由,受约束段的名称,从 URL 中提取的段变量以及请求是传入还是传出 URL(我在第16章中解释了传出 URL)。

在本例中,我使用routeKey参数从values参数中获取已应用约束的段变量的值,将其转换为小写字符串,并查看它是否匹配在静态Days字段中定义的一周中的某一天。清单15-32使用单独的技术将新的约束应用于示例路由。

清单 15-32:UrlsAndRoutes 文件夹下的 Startup.cs 文件,使用自定义约束

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing;
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "MyRoute",
                    template: "{controller}/{action}/{id?}",
                    defaults: new { controller = "Home", action = "Index" },
                    constraints: new { id = new WeekDayConstraint() });
            });
        }
    }
}

只有在id段不存在(例如 /Customer/List)或与约束类中定义的一周的某一天(例如 /Customer/List/Fri)匹配时,此路由才会匹配 URL。

定义内联自定义约束

设置自定义约束以使其可用于内联,需要一个附加的配置步骤,如清单15至33所示。

清单 15-33:UrlsAndRoutes 文件夹下的 Startup.cs 文件,使用内联自定义约束

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing;
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<RouteOptions>(options =>
                options.ConstraintMap.Add("weekday", typeof(WeekDayConstraint)));
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "MyRoute",
                    template: "{controller=Home}/{action=Index}/{id:weekday?}");
            });
        }
    }
}

ConfigureService方法中,我配置了RouteOptions对象,它控制路由系统的一些行为。ConstraintMap属性返回用于将内联约束的名称转换为提供约束逻辑的IRouteConstraint实现类的字典。我将一个新的映射添加到字典中,这样我就可以将WeekDayConstraint类内联为weekday,如下所示:

...
template: "{controller=Home}/{action=Index}/{id:weekday?}",
...

约束的效果是相同的,但是设置映射允许内联使用自定义类。

使用特性路由

到目前为止,本章中的所有示例都是使用一种称为*基于约定的路由(convention-based routing)的技术来定义的。MVC 还支持一种称为特性路由(attribute routing)*的技术,这种由 C# 特性定义的路由,可直接应用在控制器类中。在下面的小节中,我将向您展示如何使用特性创建和配置路由,这些特性可以与前面示例中所示的基于约定的路由自由混合。

准备特性路由

当您在 Startup.cs 文件中调用 UseMvc 方法时,就已经启用了特性路由。MVC 检查应用程序中的控制器类,查找任何具有路由特性的类,并为它们创建路由。

对于本章的本节,我将示例应用程序返回到《理解默认路由配置》这一部分中的描述的默认路由配置,如清单15-34所示。

清单 15-34:UrlsAndRoutes 文件夹下的 Startup.cs 文件,使用默认路由配置

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing;
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<RouteOptions>(options =>
                options.ConstraintMap.Add("weekday", typeof(WeekDayConstraint)));
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

默认路由将匹配使用以下模式的 URL:

{controller}/{action}/{id?}

应用特性路由

Route特性用于为单个控制器和 action 指定路由。在清单15-35中,我将Route特性应用于CustomerController类。

清单 15-35:Controllers 文件夹下的 CustomerController.cs 文件,应用Route特性

using Microsoft.AspNetCore.Mvc;
using UrlsAndRoutes.Models;

namespace UrlsAndRoutes.Controllers
{
    public class CustomerController : Controller
    {
        [Route("myroute")]
        public ViewResult Index() => View("Result",
            new Result
            {
                Controller = nameof(CustomerController),
                Action = nameof(Index)
            });

        public ViewResult List(string id)
        {
            Result r = new Result
            {
                Controller = nameof(HomeController),
                Action = nameof(List),
            };
            r.Data["Id"] = id ?? "<no value>";
            r.Data["catchall"] = RouteData.Values["catchall"];
            return View("Result", r);
        }
    }
}

Route特性通过定义应用于其的 action 方法或控制器的路由来工作。在清单中,我将特性应用于Index action 方法,并指定myroute作为应该使用的路由。其效果是更改用于到达 Customer 控制器定义的 action 方法的路由集,如表15-9所述。

表 15-9:Customer 控制器的路由

路由 描述
/Customer/List 此 URL 以List action 方法为目标,依赖于 Startup.cs 文件中的默认路由。
/myroute 此 URL 以Index action 方法为目标

有两点需要注意。第一,当您使用Route特性时,您为配置特性所提供的值用于定义一个完整的路由,从而使myroute成为到达Index action 方法的完整 URL。要注意的第二点是,使用Route特性可以防止使用默认的路由配置,这样就不能再使用 /Customer/Index URL 来达到Index action 方法。

更改 Action 方法的名称

在大多数应用程序中,为单个 action 方法定义唯一的路由并不有用,但是Route特性也可以更灵活地使用。在清单15-36中,我在路由中使用了专用[controller]令牌来引用控制器并设置路由的基本部分。

提示:你也可以使用ActionName特性来更改 action 名称,我将在第31中对此进行描述。

清单 15-36:Controllers 文件夹下的 CustomerController.cs 文件,重命名 Action

using Microsoft.AspNetCore.Mvc;
using UrlsAndRoutes.Models;

namespace UrlsAndRoutes.Controllers
{
    public class CustomerController : Controller
    {
        [Route("[controller]/MyAction")]
        public ViewResult Index() => View("Result",
            new Result
            {
                Controller = nameof(CustomerController),
                Action = nameof(Index)
            });

        public ViewResult List(string id)
        {
            Result r = new Result
            {
                Controller = nameof(HomeController),
                Action = nameof(List),
            };
            r.Data["Id"] = id ?? "<no value>";
            r.Data["catchall"] = RouteData.Values["catchall"];
            return View("Result", r);
        }
    }
}

Route特性的参数中使用[controller]令牌就像使用一个nameof表达式,它允许在不对类名进行硬编码的情况下指定到控制器的路由。表15-10描述了清单15-36中特性的效果。

15-10 :Customer 控制器的路由

路由 描述
/Customer/List 此 URL 以List action 方法为目标
/Customer/MyAction 此 URL 以Index action 方法为目标

创建一个更复杂的路由

Route特性也可以应用于控制器类,允许定义路由的结构,如清单15-37所示。

清单 15-37:Controllers 文件夹下的 CustomerController.cs 文件,应用Route特性

using Microsoft.AspNetCore.Mvc;
using UrlsAndRoutes.Models;

namespace UrlsAndRoutes.Controllers
{
    [Route("app/[controller]/actions/[action]/{id?}")]
    public class CustomerController : Controller
    {
        public ViewResult Index() => View("Result",
            new Result
            {
                Controller = nameof(CustomerController),
                Action = nameof(Index)
            });

        public ViewResult List(string id)
        {
            Result r = new Result
            {
                Controller = nameof(HomeController),
                Action = nameof(List),
            };
            r.Data["Id"] = id ?? "<no value>";
            r.Data["catchall"] = RouteData.Values["catchall"];
            return View("Result", r);
        }
    }
}

该路由定义混合了静态段和变量段,并使用[controller][action]令牌来引用控制器类的名称和 action 方法。表15-11显示了路由的效果。

表 15-11:Customer 控制器的路由

路由 描述
app/customer/actions/index 此 URL 以Index action 方法为目标
app/customer/actions/index/myid 此 URL 以Index action 方法为目标,并将可选id段设置为myid
app/customer/actions/list 此 URL 以List action 方法为目标
app/customer/actions/list/myid 此 URL 以List action 方法为目标,并将可选id段设置为myid

应用路由约束

使用特性定义的路由可以被约束,就像 Startup.cs 文件中定义的路由一样,使用用于基于约定的路由的内联技术。在清单15-38中,我已经将本章前面创建的自定义约束应用于使用Route特性定义的可选id段。

清单 15-38:Controllers 文件夹下的 CustomerController.cs 文件,约束路由

using Microsoft.AspNetCore.Mvc;
using UrlsAndRoutes.Models;

namespace UrlsAndRoutes.Controllers
{
    [Route("app/[controller]/actions/[action]/{id:weekday?}")]
    public class CustomerController : Controller
    {
        public ViewResult Index() => View("Result",
            new Result
            {
                Controller = nameof(CustomerController),
                Action = nameof(Index)
            });

        public ViewResult List(string id)
        {
            Result r = new Result
            {
                Controller = nameof(HomeController),
                Action = nameof(List),
            };
            r.Data["Id"] = id ?? "<no value>";
            r.Data["catchall"] = RouteData.Values["catchall"];
            return View("Result", r);
        }
    }
}

您可以使用表15-8中描述的所有约束,或者,如清单所示,使用已在RouteOptions服务中注册的自定义约束。通过将它们链接在一起并用冒号分隔它们,可以应用多个约束。

总结

在本章中,我深入研究了路由系统。您已经看到了路由是如何由约定或特性定义的。您已经看到了如何匹配和处理传入 URL,以及如何通过更改它们与 URL 段匹配的方式以及使用默认值和可选段来自定义路由。我还向您展示了如何限制路由以缩小它们将匹配的请求范围,包括使用内置约束和使用自定义约束类。

在下一章中,我将向您展示如何从视图中的路由生成传出 URL,以及如何使用 areas 特性,它依赖于路由系统,可以用于管理大型和复杂的 MVC 应用程序。

;

© 2018 - IOT小分队文章发布系统 v0.3